﻿function ParseXml(area) {
    this.tx = area;
    this.st = 0; // parse start
    this.ts = 0; // tag start
    this.te = 0; // tag body end
    this.ie = 0; // inner end
    this.en = 0; // whole tag end
    this.me = 0; // missing end
};

ParseXml.Cache = {};
// Intentionally cut attributes: hspace|vspace|usemap|ismap|text|background|bgcolor|alink|vlink
ParseXml.Attributes = 'id|class|style|title|alt|type|width|height|size|href|src|language|clear|align|valign|name|value|checked|selected|maxlength|multiple|rows|cols|action|method|enctype|http-equiv|content|rel|rev|border|cellspacing|cellpadding|rowspan|colspan|noshad|nowrap';
ParseXml.SplitAttributes = ParseXml.Attributes.split('|');
ParseXml.Lonelies = 'img|hr|br|meta|link|base|isindex|input|basefont|param|area|!doctype';
ParseXml.LoneRx = new RegExp('^(' + ParseXml.Lonelies + ')$', '');
ParseXml.IsLonely = function (n) { return ParseXml.LoneRx.test(n) };
ParseXml.RxTag = /(?:<([!?]?[A-Za-z][A-Za-z0-9]*:*[A-Za-z0-9]*)\s*?((?:"[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*'|[^\/>])*?)(\/>|>))|(?:<!--([\s\S]*?)-->)|(?:<\/([A-Za-z][A-Za-z0-9]*?)>)/g;
ParseXml.RxAttr = /([A-Za-z][-_A-Za-z0-9]*)=("[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*'|[-_A-Za-z0-9]+)|\b([A-Za-z][-_A-Za-z0-9]*)\b/g;
ParseXml.lower = function (t) { return t.toLowerCase() };
ParseXml.upper = function (t) { return t.toUpperCase() };
ParseXml.same = function (t) { return t };
ParseXml.none = function (po) { return false };
ParseXml.lonely = function (po) { return ParseXml.IsLonely(po.tagName) };
ParseXml.prototype.ignore = function (n) { return /^[!?]|^(script|style|xmp)$/.test(n) };

ParseXml.Parse = function (area, cf) {
    var po = new ParseXml(area);
    if (!area) return po;
    var rx, mm, li, ct;
    po.cf = cf = ParseXml.init(cf);
    ct = cf.controller;
    po.append = function (t) { ct.append.call(ct, t) };
    po.buffer = function () { return ct.buffer.call(ct) };
    po.controller = ct;
    rx = ParseXml.RxTag;
    rx.lastIndex = 0;
    mm = rx.exec(po.tx);
    while (mm != null) {
        po.attr = po.style = po.tagName = po.tagAttr = po.tagEndr = po.comment = po.floater = null;
        li = rx.lastIndex;
        po.ts = mm.index;
        po.ie = po.te = mm.index + mm[0].length;
        if (mm[1]) po.tagName = cf.usecase(mm[1].toString());
        if (mm[2]) po.tagAttr = mm[2].toString();
        if (mm[3]) po.tagEndr = mm[3].toString();
        if (mm[4]) { po.comment = mm[4].toString(); po.tagName = '!--'; po.tagAttr = po.comment; po.tagEndr = '-->' };
        if (mm[5]) { po.floater = mm[5].toString(); po.tagName = '/' + po.floater; po.tagAttr = ''; po.tagEndr = '>' };
        if (po.tagEndr == '>' && !cf.lonely(po) && !po.floater) {
            var ix = ParseXml.FindEnd(po.tx.substr(mm.index), po.tagName);
            if (ix > 0) {
                po.ie = mm.index + ix;
                li = po.en = mm.index + ix + po.tagName.length + 3;
            } else {
                if (ix == 0) li = po.te + po.tagName.length + 3;
                else if (ix == -2) po.me = area.length;
            }
        }
        po.en = li;
        cf.callback(po);
        rx.lastIndex = po.st = li;
        if (po.stop) break;
        mm = rx.exec(po.tx);
    }
    if (po.st < po.tx.length && !po.stop) {
        po.attr = po.tagName = po.tagAttr = po.tagEndr = po.comment = po.floater = null;
        po.en = po.ts = po.te = po.ie = po.tx.length;
        po.tagName = 'remaining_area';
        cf.callback(po);
    }
};

ParseXml.init = function (cf) {
    if (typeof cf == 'function') cf = { callback: cf };
    if (!cf.controller) cf.controller = {};
    if (!cf.usecase) cf.usecase = ParseXml.lower;
    if (!cf.lonely) cf.lonely = ParseXml.lonely;
    return cf;
};

ParseXml.ParseInner = function (po) {
    if (po.ignore(po.tagName)) {
        po.append(po.getFront().reduceSp() + po.getOuter());
        return;
    }
    var inner = po.getInner();
    if (inner) {
        po.append(po.getFront() + po.getTag());
        ParseXml.Parse(inner, po.cf);
        po.append(po.getEndTag());
    }
    else
        po.append(po.getFront() + po.getOuter());
};

ParseXml.ParseRecur = function (po) {
    var inner = po.getInner();
    if (inner) ParseXml.Parse(inner, po.cf);
};

// Use instead of ParseInner if tagName or attrs change
ParseXml.BuildInner = function (po) {
    if (po.ignore(po.tagName)) {
        po.append(po.getFront().reduceSp() + po.getOuter());
        return;
    }
    var inner = po.getInner();
    po.append(po.getFront() + po.buildTag());
    if (inner) ParseXml.Parse(inner, po.cf);
    po.append(po.getEndTag());
};

ParseXml.ParseAttr = function (area) {
    var an, aa = {}, bb = {};
    if (!area) return [aa, bb];
    var rx = ParseXml.RxAttr;
    rx.lastIndex = 0;
    var mm = rx.exec(area);
    while (mm != null) {
        if (mm[1]) {
            an = mm[1].toLowerCase();
            aa[an] = mm[2].replace(/^["']|["']$/g, '');
            bb[an] = mm[1];
        }
        else if (mm[3]) {
            an = mm[3].toLowerCase();
            aa[an] = true;
            bb[an] = mm[3];
        };
        mm = rx.exec(area);
    }
    return [aa, bb];
};

ParseXml.FindEnd = function (area, tg) {
    var ct = 0;
    if (tg.charAt(0) == '!') return -1;
    if (!ParseXml.Cache[tg])
        ParseXml.Cache[tg] = new RegExp('(<' + tg + '\\s*?(?:"[^"\\\\]*(?:\\\\[\\s\\S][^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\[\\s\\S][^\'\\\\]*)*\'|[^>])*>)|(<\\/' + tg + '>)|(<!--[\\s\\S]*?-->)', 'ig');
    var rx = ParseXml.Cache[tg];
    rx.lastIndex = 0;
    var mm = rx.exec(area);
    while (mm != null) {
        if (mm[1]) ct++;
        if (mm[2]) ct--;
        if (!mm[3] && ct <= 0) return mm.index; // When not a comment
        mm = rx.exec(area);
    }
    return -2;
};

ParseXml.prototype.getFront = function () {
    if (this.ts > this.st) return this.tx.substring(this.st, this.ts);
    return '';
};

ParseXml.prototype.getWhite = function () { return this.getFront().replace(/^.*?(\s*)$/, '$1') };

ParseXml.prototype.getTag = function () {
    if (this.te > this.ts) return this.tx.substring(this.ts, this.te);
    return '';
};

ParseXml.prototype.hasInner = function () { return this.ie > this.te };

ParseXml.prototype.getInner = function () {
    if (this.ie > this.te) return this.tx.substring(this.te, this.ie);
    return '';
};

ParseXml.prototype.getOuter = function () {
    if (this.en > this.ts) return this.tx.substring(this.ts, this.en);
    return '';
};

ParseXml.prototype.getEndTag = function () {
    if (this.en > this.ie) return this.tx.substring(this.ie, this.en);
    return '';
};

ParseXml.prototype.getAttr = function (n) {
    if (!this.attr) this.attr = ParseXml.ParseAttr(this.tagAttr);
    return this.attr[0][n.toLowerCase()];
};

ParseXml.prototype.setAttr = function (n, v) {
    if (!this.attr) this.attr = ParseXml.ParseAttr(this.tagAttr);
    var an = n.toLowerCase();
    if (v === undefined || v === null) {
        delete this.attr[0][an];
        delete this.attr[1][an];
    }
    else {
        this.attr[0][an] = v;
        this.attr[1][an] = this.attr[1][an] || n;
    }
};

ParseXml.prototype.removeClass = function (cls) {
    var c = (this.getAttr('class') || '').trim();
    if (c) {
        c = c.replace(new RegExp('(\\s|\\b)' + cls.escapeRegExp() + '(\\s|\\b)'), '');
        this.setAttr('class', c);
    }
    else {
        c = null;
        this.setAttr('class', c);
    }
    return c;

};

ParseXml.prototype.addClass = function (cls) {
    var c = (this.getAttr('class') || '').trim();
    if (c) c = this.removeClass(cls) || '';
    c += (c ? ' ' : '') + cls;
    this.setAttr('class', c);
    return c;
};

ParseXml.prototype.getStyleMap = function () {
    //if (this.style) return this.style;
    this.style = {};
    var s = this.getAttr('style');
    if (s) {
        var p = s.split(';');
        for (var i = 0; i < p.length; i++) {
            if (!p[i]) continue;
            var pp = p[i].split(':');
            if (pp[0] && pp[1]) this.style[pp[0].camelize()] = pp[1];
        }
    }
    return this.style;
};

// Please don't expose this!!!
//ParseXml.prototype.getAttrOb = function() {
//	if (!this.attr) this.attr = ParseXml.ParseAttr(this.tagAttr);
//	return this.attr[0];
//}

ParseXml.prototype.buildTag = function () {
    if (this.tagName == 'remaining_area') return '';
    return '<' + this.tagName + this.buildAttr() + this.tagEndr;
};

ParseXml.prototype.buildAttr = function () {
    if (!this.attr && !this.style) return '';
    var stl = this.buildStyle();
    if (this.style) this.setAttr('style', stl || null);
    var ao = this.attr[0], bo = this.attr[1], ot = '';
    for (var n in ao) {
        ot += ' ' + bo[n] + '="' + ao[n] + '"';
    }
    return ot;
};

ParseXml.prototype.buildStyle = function () {
    if (!this.style) return '';
    var ss = this.style, ot = '';
    for (var n in ss) {
        if (ss[n]) ot += n.uncamelize() + ':' + ss[n] + ';';
    }
    return ot;
};

//
// ProcessXml object
// Creates new instance to hold processing variables so can be used multiple times in parallel
//

function ProcessXml() { };

ProcessXml.Process = function (content, map) {
    return (new ProcessXml()).process(content, map);
};

ProcessXml.prototype.process = function (content, map) {
    this.oot = '';
    this.changed = false;
    this.map = map || {};
    ParseXml.Parse(content,
	{
	    controller: this,
	    callback: jQuery.proxy(this, 'parseTag')
	});
    this.output = this.oot;

    // This check is VERY EXPENSIVE ... maybe could rethink it
    if (content != this.output) this.changed = true;

    return this;
};

ProcessXml.prototype.parseTag = function (po) {
    var tn = po.tagName.replace(/^!/, '$').replace(/^\$--/, 'comment');
    var me = this.map[tn];
    if (!me) {
        tn = 'any';
        me = this.map[tn];
        // Ignore script, style, xmp, ?, and ! tags for "any" calls
        // Add explicit definitions to include them
        if (me && po.ignore(po.tagName)) {
            po.append(po.getFront().reduceSp() + po.getOuter());
            return;
        }
    }

    switch (typeof (me)) {

        case 'function':
            this.map[tn](po);
            break;

        case 'string':
            this.oot += po.getFront() + me;
            break;

        case 'object':
        case 'number':
            throw new Error('ProcessXml() mapping to a number or object is invalid');

        case 'undefined':
        default:
            ParseXml.ParseInner(po);
            break;
    }
};

ProcessXml.prototype.buffer = function () { return this.oot };
ProcessXml.prototype.append = function (t) { this.oot += t };

String.prototype.reduce = function () { return this.replace(/^\s+|\s+$/g, ' ') };
String.prototype.reduceSp = function () {
    var bg, en = '', ot = this.trim();
    bg = this.extract(/^(\s+)/);
    if (ot) en = this.extract(/(\s+)$/);
    if (/\n/.test(bg)) ot = '\n' + ot;
    else if (bg) ot = ' ' + ot;
    if (/\n/.test(en)) ot += '\n';
    else if (en) ot += ' ';
    return ot;
};
